package tool.codescan;

import japa.parser.ast.ImportDeclaration;
import japa.parser.ast.type.ClassOrInterfaceType;
import japa.parser.ast.type.PrimitiveType;
import japa.parser.ast.type.ReferenceType;
import japa.parser.ast.type.Type;
import japa.parser.ast.type.VoidType;
import japa.parser.ast.type.WildcardType;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import tool.codescan.mappers.InnerClassMapper;
import tool.codescan.mappers.JavaImplicitImportMapper;
import tool.codescan.mappers.PackageMapper;
import tool.csv.FileWriter;
import tool.data.TypeDefinition;

	/**
	 * @author Jamie Stevenson
	 * @date 2013/11/28
	 * 
	 * This class is a clearing house for type-grounding requests.  ALL type resolving actions
	 * should be done via this class or delegated to a 'wrapped' class.
	 * 
	 * Types are handled as {@link String} in two main formats:
	 * - The plain type name which is usually capitalised, but defers to the writers style
	 * - The 'full name' of the type which takes the form 'package*.Type'.  Note the lack of
	 *   spaces, trailing semi-colon or import statement prefix.
	 *   
	 */

public class TypeResolver {
    
    //TODO - fix this static import to be a local jar resource
    private final String pathToJavaSource = "C:" + File.separatorChar +
                                            "Users" + File.separatorChar +
                                            "jwb09119" + File.separatorChar +
                                            "Desktop" + File.separatorChar +
                                            "javaparser-1.0.8-src" + File.separatorChar +
                                            "jimt" + File.separatorChar +
                                            "src" + File.separatorChar +
                                            "lib" + File.separatorChar +
                                            "java1.7.0_45" + File.separatorChar;
    
	private JavaImplicitImportMapper javaImplicitImports;
	private PackageMapper packageImports;
	private List<PackageMapper> dependencyTrees;
	private InnerClassMapper innerClassResolver;
	
	public TypeResolver (PackageMapper packageMap, List<File> dependencies, InnerClassMapper icm) {
		
		packageImports = packageMap;
		javaImplicitImports = new JavaImplicitImportMapper();
		dependencyTrees = generateDependencyTrees(dependencies);
		innerClassResolver = icm;
		
	}
	
	
	private final List<PackageMapper> generateDependencyTrees (List<File> dependencies) {
	    
	    List<PackageMapper> reply = new ArrayList<>();
	    
	    addJavaLibraryTrees(reply);
	    
	    for (File f : dependencies) {
	        
	        PackageMapper pm  = new PackageMapper(f);
	        reply.add(pm);
	        
	    }
	    
	    return reply;
	    
	}
	
	
	private final void addJavaLibraryTrees (List<PackageMapper> treesAddedThis) {
	
	    File javaSourceLocation = new File(pathToJavaSource);
	    
	    if (!javaSourceLocation.exists()) {
	        
	        FileWriter.writeToLogFile("TypeResolver can't find java source files at: " + javaSourceLocation);
	        
	    }
	    
	    if (javaSourceLocation.isDirectory()) {
	    
    	File[] dirs = javaSourceLocation.listFiles();
    	    
    	    for (File f : dirs) {
    	        
    	        treesAddedThis.add(new PackageMapper(f));
    	        
    	    }
	    
	    }
	    
	}
	
	
	/**
	 * @param name - the object representing the name of the type being looked for. 
	 * @param packageName - the string representation of the package where the Type represented by
	 *                      name is located
	 * @param imports - a List of Import Declarations which may contain a reference to name
     * @return - TypeDefinition - updated object representing the name of the parameter if any 
     *           relevant path information is found.
	 *         
	 * This method will employ several decreasingly specific approaches to find a matching type. 
	 * This includes checking through implicit Java imports and attempting to resolve star 
	 * imports.
	 */
	public TypeDefinition groundTypeToSpecificImport (TypeDefinition name, 
	                                   List<ImportDeclaration> imports, String containingPackage) {

		TypeDefinition reply = checkImportListForType(name, imports);
		
		if (!reply.hasResolvedPath()) {
		    
		    reply = checkForLocalPackageType(name, containingPackage);
		    
		}
		
		if (!reply.hasResolvedPath()) {
			
			reply = javaImplicitImports.findImplicitImportType(name);
			
		}
		
		if (!reply.hasResolvedPath()) {
			
			reply = checkForStarImport(name, imports);
			
		}

		// path may still be unresolved
		
		return reply;
		
	}

	


    /**
	 * Checks the supplied list of imports for a specified Type name
	 * @param name - the object representing the name of the Type being searched for
	 * @param imports - the list of imports for the context of the looked-for Type
     * @return - TypeDefinition - updated object representing the name of the parameter if any 
     *           relevant path information is found.
	 */
	public TypeDefinition checkImportListForType (TypeDefinition name, List<ImportDeclaration> imports) {
		
		TypeDefinition reply = name;
		
		for (ImportDeclaration anImport : imports) {
			
			// It's a direct import.
			if (!anImport.isAsterisk() && !name.hasResolvedPath()) {
				
				String whatIsImported = cleanUpImportFormat(anImport);
				int lastDotIndex = whatIsImported.lastIndexOf('.');
				String importName = whatIsImported.substring(lastDotIndex+1, whatIsImported.length());
				
				// Check explicit imports
				if (name.getName().equals(importName)) {
					
				    String packagePath = whatIsImported.substring(0, lastDotIndex);
					reply.setPath(packagePath);
					break;
					
				}
				
			    reply = checkForInnerClass(name, whatIsImported);

				reply = checkForDependencyImport(reply, whatIsImported);

			}
			
		}
		
		return reply;
		
	}
	
	
	private TypeDefinition checkForInnerClass (TypeDefinition name, String anImport) {
	    
	    // Check if this explicit import has any inner types.
		if(innerClassResolver.checkForInnerClasses(anImport)) {
		    
		    List<TypeDefinition> candidateInnerClasses = innerClassResolver.getInnerClasses(anImport);
		    
		    for (TypeDefinition ic : candidateInnerClasses) {
		        
		        if (ic.getName().endsWith(name.getName())) {
		            
		            name.addInnerClassPrefix();
		            name.setPath(ic.getPath());
		            break;
		            
		        }
		        
		    }
		    
		}
		
		return name;
	    
	}
	
	
	/**
	 * Checks the same package as the containing type for a matching (local) type
     * @param name - the string representation of the Type being looked for
	 * @param packageName - the string representation of the package
     * @return - TypeDefinition - updated object representing the name of the parameter if any 
     *           relevant path information is found.
	 */
	private TypeDefinition checkForLocalPackageType (TypeDefinition name, String maybePackage) {

        return packageImports.resolvePackageForType(name, maybePackage);
        
	}
	
	
	/**
	 * Checks the local application for a matching import Type 
	 * @param name - the object representing the Type being looked for
	 * @param imports - the list of imports from the containing compilation unit
     * @return - TypeDefinition - updated object representing the name of the parameter if any 
     *           relevant path information is found.
	 */
	private TypeDefinition checkForStarImport (TypeDefinition name, List<ImportDeclaration> imports) {
		
	    TypeDefinition reply = name;
		
		for (ImportDeclaration anImport : imports) {
			
			if (anImport.isAsterisk()) {
				
				String whatIsImported = cleanUpImportFormat(anImport);
				
				// Check for import from elsewhere in local source
				reply = packageImports.resolvePackageForType(reply, whatIsImported);
				
				if (reply.hasResolvedPath()) {
				     
				    break; 
				    
				}
				
				reply = checkForDependencyImport(reply, whatIsImported);
		
			}
		
		}
		
		return reply;
		
	}
	

	/**
	 * Check pre-defined dependencies (libraries, etc) for a specified type
	 * @param name - the name of the type to check for
	 * @param importString - the import declaration to search for the Type represented by name
	 * @return - TypeDefinition - updates the name parameter if any relevant path information is
	 *           found. 
	 */
	private TypeDefinition checkForDependencyImport (TypeDefinition name, String importString) {
	    
	    TypeDefinition reply = name;
	    
	    
	    for (int i = 0; i < dependencyTrees.size() && !reply.hasResolvedPath(); i++) {
	        
	        reply = dependencyTrees.get(i).resolvePackageForType(name, importString);
	        
	    }
	    
	    return reply;
	    
	}
	
	
	/**
	 * Converts an ImportDeclaration compilation unit object to a more usable string format.
	 * @param importDecl - representing an import in a java file
	 * @return - String - in the form 'package*.TypeName'
	 * 
	 * NB - leading import or static declarations are removed, asterisk imports are NOT appended to 
	 * the string.
	 */
	private String cleanUpImportFormat (ImportDeclaration importDecl) {
		
		String whatIsImported = importDecl.getName().toString();
		
		// Remove initial "import (static?) " and trailing ';'
		whatIsImported = whatIsImported.substring(whatIsImported.lastIndexOf(' ')+1, 
																	whatIsImported.length());
		
		return whatIsImported;
		
	}
	
	
	/**
	 * Extract a type name String from a Type object.
	 * @param type - an AST component representing a type
	 * @return String - representation of the type, in the case of most types, this will be the
	 *                  type as it was defined by the writer e.g. "MyType"
	 *                - Primitives are represented as "PRIMITIVE.Type"
	 *                - If the type contains no name data, returns "UNRESOLVED_TYPE"
	 */
	public static String getTypeName (Type type) {
		
    	String reply = "UNRESOLVED_TYPE";
    	
    	if (type instanceof ClassOrInterfaceType) {
    		
    		reply =  ((ClassOrInterfaceType) type).getName();
    		
    	} else if (type instanceof PrimitiveType) {
    		
    		reply = "PRIMITIVE."+((PrimitiveType) type).getType().toString();
    		
    	} else if (type instanceof ReferenceType) {
    		
    		reply = getTypeName(((ReferenceType) type).getType());
    		
    	} else if (type instanceof VoidType) {
    		
    		reply = "PRIMITIVE.VOID";
    		
    	} else if (type instanceof WildcardType) {
    		
    		reply = ((WildcardType) type).toString();
    		
    	}
    	
    	return reply;
		
	}
}
